昨天提到,純向量檢索對「精確關鍵字、專有名詞、代號」不一定敏感。今天要介紹的 Hybrid Search(混合檢索),就是為了截長補短:同時使用關鍵字搜尋與向量搜尋,再把兩種結果融合排序。
| 檢索方式 | 擅長 | 不擅長 |
|---|---|---|
| 關鍵字搜尋(如 BM25) | 精確匹配專有名詞、代號、數字 | 無法理解同義詞、換句話說的語意相近性 |
| 向量搜尋 | 理解語意相近、同義詞、換句話說 | 對精確字串匹配不敏感 |
兩者結合,可以同時保有「語意理解」與「精確匹配」的優勢。
BM25 是一種經典的全文檢索排序演算法,核心概念是:一個詞在某文件中出現頻率越高、但在整個資料庫中越罕見,代表這個詞對這篇文件越重要(類似 TF-IDF 的進階版)。
Postgres 內建的全文檢索(Full Text Search)就是基於類似的原理:
-- 建立全文檢索索引
ALTER TABLE chunks ADD COLUMN content_tsv tsvector
GENERATED ALWAYS AS (to_tsvector('simple', content)) STORED;
CREATE INDEX idx_content_tsv ON chunks USING GIN (content_tsv);
-- 關鍵字搜尋查詢
SELECT content, ts_rank(content_tsv, query) AS rank
FROM chunks, to_tsquery('simple', 'E1024') query
WHERE content_tsv @@ query
ORDER BY rank DESC
LIMIT 5;
常見的融合策略是 RRF(Reciprocal Rank Fusion,倒數排名融合):把兩種檢索方式各自產生的排名,轉換成分數後加總,不需要煩惱兩種分數量級不同(BM25 分數與 Cosine 相似度數值範圍完全不同)的問題。
def reciprocal_rank_fusion(rank_lists, k=60):
"""
rank_lists: [[doc_id1, doc_id2, ...], [doc_id3, doc_id1, ...]]
每個 list 是一種檢索方式依相關度排序後的文件 id 清單
"""
scores = {}
for rank_list in rank_lists:
for rank, doc_id in enumerate(rank_list):
scores[doc_id] = scores.get(doc_id, 0) + 1 / (k + rank + 1)
return sorted(scores.items(), key=lambda x: x[1], reverse=True)
def hybrid_search(question, top_k=5):
vector_results = search_similar_chunks(question, top_k=20) # 向量搜尋,取較多候選
keyword_results = keyword_search(question, top_k=20) # 關鍵字搜尋,取較多候選
vector_ids = [r["id"] for r in vector_results]
keyword_ids = [r["id"] for r in keyword_results]
fused = reciprocal_rank_fusion([vector_ids, keyword_ids])
top_ids = [doc_id for doc_id, score in fused[:top_k]]
return fetch_chunks_by_ids(top_ids)
如果知識庫內容偏向一般性的概念說明、使用者也習慣用自然語言提問,單純向量搜尋可能就已經足夠,不一定要為了 Hybrid Search 增加系統複雜度。
Hybrid Search 透過融合關鍵字與向量兩種檢索方式,補足了純向量搜尋在精確匹配上的弱點。是否需要導入,建議依照自己知識庫的內容特性與使用者提問習慣來判斷,而不是照單全收。明天我們要介紹另一個提升檢索精準度的重要技巧:Rerank。